<script lang="ts">
  import { Tooltip, Alert, Button } from 'svelte-5-ui-lib'
  import { onMount, onDestroy, tick } from 'svelte'
  import { sineIn } from 'svelte/easing'
  import { PaperClipOutline, CodeOutline, CompressOutline } from 'flowbite-svelte-icons'
  import 'prismjs/themes/prism-okaidia.css'
  import 'prismjs/plugins/line-numbers/prism-line-numbers'
  import 'prismjs/plugins/line-numbers/prism-line-numbers.js'
  import { highlight, languages } from 'prismjs'
  import CopyButton from '$lib/components/IconComponents/CopyButton.svelte'
  import { SUPPORTED_LANGS } from './lang-config'
  import { throttle } from '$lib/utils/functionHelper'
  import type { CodeType } from './code-types'

  let {
    lang = $bindable('json'),
    rawText = $bindable(''),
    uihelpers,
    containerDivClass = 'max-h-[300px]',
  }: CodeType = $props()

  let formatError: boolean | string = $state(false)
  let editorElement: HTMLDivElement | null = $state(null)
  let lineNumbersElement: HTMLDivElement | null = $state(null)
  let codeDivElementHeight = $state(0)
  let codeDivElementScrollHeight = $state(0)
  let highlightedHtml = $state('')

  $effect(() => {
    SUPPORTED_LANGS[lang]?.prism?.().then(() => {
      try {
        highlightedHtml = highlight(rawText, languages[lang], lang)
      } catch {
        highlightedHtml = rawText
      }
    })
  })

  $effect(() => {
    try {
      highlightedHtml = highlight(rawText, languages[lang], lang)
    } catch {
      highlightedHtml = rawText
    }
    updateContent()
  })

  async function updateContent(newText: string | null = null) {
    const target = editorElement
    const sel = window.getSelection()
    let cursorOffset = 0
    if (sel?.rangeCount && target) {
      const range = sel.getRangeAt(0)
      cursorOffset = getCursorOffset(target, range)
    }
    if (newText !== null) {
      rawText = newText as string
    }
    await tick()
    await new Promise(requestAnimationFrame)
    if (editorElement) restoreCursor(editorElement, cursorOffset)
  }

  const handleInput = (e: Event) => {
    const target = e.target as HTMLDivElement
    // 只用 textContent 会丢失换行和格式，需用 innerText 保留换行
    updateContent(target.innerText || '')
  }

  function getCursorOffset(container: Node, range: Range): number {
    const preRange = document.createRange()
    preRange.selectNodeContents(container)
    preRange.setEnd(range.startContainer, range.startOffset)
    return preRange.toString().length
  }

  function restoreCursor(container: Node, offset: number) {
    const sel = window.getSelection()
    if (!sel) return
    const range = createRangeFromOffset(container, offset)
    sel.removeAllRanges()
    sel.addRange(range)
  }

  function createRangeFromOffset(container: Node, targetOffset: number): Range {
    const range = document.createRange()
    let currentOffset = 0
    let found = false
    const walk = (node: Node): boolean => {
      if (found) return true
      if (node.nodeType === Node.TEXT_NODE) {
        const text = node.textContent || ''
        const textLength = text.length
        if (currentOffset <= targetOffset && currentOffset + textLength >= targetOffset) {
          range.setStart(node, targetOffset - currentOffset)
          range.collapse(true)
          found = true
          return true
        }
        currentOffset += textLength
        return false
      } else if (node.nodeType === Node.ELEMENT_NODE) {
        for (const child of node.childNodes) {
          if (walk(child)) return true
        }
      }
      return false
    }
    walk(container)
    if (!found) {
      range.selectNodeContents(container)
      range.collapse(false)
    }
    return range
  }

  const syncScroll = throttle(() => {
    if (lineNumbersElement) {
      codeDivElementScrollHeight = editorElement?.scrollTop || 0
      lineNumbersElement.style.height = `${lineDivElementHeight}px`
    }
  }, 50)

  let resizeObserver: ResizeObserver | null = null

  onMount(() => {
    if (!editorElement) return
    if (!resizeObserver) {
      resizeObserver = new ResizeObserver((entries) => {
        if (!editorElement) return
        const entry = entries.find((e) => e.target === editorElement)
        if (entry) {
          codeDivElementHeight = entry.contentRect.height
        }
      })
    }
    resizeObserver.disconnect()
    resizeObserver.observe(editorElement)
    editorElement.addEventListener('scroll', syncScroll, { passive: true })
  })

  onDestroy(() => {
    if (resizeObserver && editorElement) {
      resizeObserver.unobserve(editorElement)
      resizeObserver.disconnect()
      resizeObserver = null
    }
    if (editorElement) {
      editorElement.removeEventListener('scroll', syncScroll)
    }
    if ('handle' in syncScroll && syncScroll.handle) {
      clearTimeout(syncScroll.handle as number)
    }
  })

  let lineDivElementHeight = $derived(codeDivElementScrollHeight + codeDivElementHeight)
  let lines = $derived(Math.floor(lineDivElementHeight / 20))
</script>

<div class="w-full h-full relative">
  <div class="absolute top-0 right-2 z-100 border-box w-[12px]">
    <div class="flex flex-col items-end gap-2">
      <Button onclick={uihelpers?.toggle} class="!bg-transparent !outline-none !ring-0 !focus:outline-none !focus:ring-0 !active:outline-none !active:ring-0 rounded-lg shadow-sm hover:bg-orange-50 dark:hover:bg-gray-800 transition-all duration-150" size="xs">
        <CompressOutline class="h-5 w-5 hover:text-orange-600 dark:hover:text-orange-500" id="maxsizeWin" />
        <Tooltip position="left" offset={6} triggeredBy="#maxsizeWin">Maxsize</Tooltip>
      </Button>
      <Button class="!bg-transparent !outline-none !ring-0 !focus:outline-none !focus:ring-0 !active:outline-none !active:ring-0 rounded-lg shadow-sm hover:bg-orange-50 dark:hover:bg-gray-800 transition-all duration-150" size="xs">
        <CodeOutline
          id="FormatCode"
          class="h-5 w-5 hover:text-orange-600 dark:text-white dark:hover:text-orange-500"
          onclick={() => {
            try {
              // 先用 formatter 格式化，再高亮
              console.log('formatting...', rawText)
              const formatted = SUPPORTED_LANGS[lang]?.formatter?.(rawText)
              console.log('formatted formatting...', formatted)
              if (typeof formatted === 'string') {
                rawText = formatted
                highlightedHtml = highlight(rawText, languages[lang], lang)
                formatError = false
              } else {
                formatError = '格式化失败'
              }
            } catch (e) {
              formatError = String(e)
            }
          }}
        />
        <Tooltip position="left" offset={6} triggeredBy="#FormatCode">Format</Tooltip>
      </Button>
      <Button class="!bg-transparent !outline-none !ring-0 !focus:outline-none !focus:ring-0 !active:outline-none !active:ring-0 rounded-lg shadow-sm hover:bg-orange-50 dark:hover:bg-gray-800 transition-all duration-150" size="xs">
        <PaperClipOutline class="h-5 w-5 rotate-45" id="fileUpload" />
        <Tooltip position="left" offset={6} triggeredBy="#fileUpload">File Upload</Tooltip>
      </Button>
      <Button class="!bg-transparent !outline-none !ring-0 !focus:outline-none !focus:ring-0 !active:outline-none !active:ring-0 rounded-lg shadow-sm hover:bg-orange-50 dark:hover:bg-gray-800 transition-all duration-150" size="xs">
        <CopyButton
          text={rawText}
          copyIconClass="h-5 w-5 text-primary hover:text-orange-600 dark:hover:text-orange-500"
        />
      </Button>
    </div>
  </div>

  <select
    name="lang-options"
    bind:value={lang}
    class="absolute bottom-2 right-2 text-right !text-xs !bg-transparent text-gray-100 border-none rounded-md shadow-sm focus:ring-2 focus:ring-orange-400 dark:focus:ring-orange-500 transition-all duration-150"
  >
    {#each Object.entries(SUPPORTED_LANGS) as [langKey, langConfig], i}
      <option value={langKey} class="bg-gray-900 dark:bg-gray-800 text-gray-100 dark:text-gray-200">
        {langConfig.name}
      </option>
    {/each}
  </select>

  <div
    bind:this={editorElement}
    class="{containerDivClass} border-box flex bg-gray-900 dark:bg-gray-950 rounded-xl border border-gray-700 dark:border-gray-800 overflow-y-auto thin-scrollbar shadow-lg"
  >
    <div
      bind:this={lineNumbersElement}
      class="line-numbers flex-none inline-block space-y-[0.375rem] pr-2 min-w-[3em] pt-4 pb-4 text-right font-mono text-sm text-gray-400/80 bg-gray-800/50 dark:bg-gray-900/40 border-r border-gray-600 dark:border-gray-700 select-none scroll-smooth rounded-tl-xl rounded-bl-xl"
    >
      {#each Array(lines) as _, i}
        <div class="line-number">
          {i + 1}
        </div>
      {/each}
    </div>

    <div
      role="textbox"
      tabindex="0"
      class="code-editor border-box language-{lang} border-none bg-gray-900 dark:bg-gray-950 h-full text-[14px]/[20px] rounded-tr-xl rounded-br-xl flex-1 p-4 pr-[45px] font-mono text-sm leading-relaxed text-gray-100 dark:text-gray-100 whitespace-pre-wrap [tab-size:2] focus:outline-none focus:ring-2 focus:ring-orange-400 dark:focus:ring-orange-500 transition-all duration-150"
      contenteditable="true"
      oninput={handleInput}
      spellcheck={false}
      bind:innerHTML={highlightedHtml}
    ></div>
  </div>

  <Alert
    color="orange"
    hidden={!formatError}
    class="absolute h-[14px] text-[14px]/[14px] -top-[14px] left-[36px] bg-transparent hover:bg-transparent "
    params={{ duration: 500, easing: sineIn, x: -150 }}
  >
    {formatError}
  </Alert>
</div>

<style>
  :root {
    --line-height: 20px;
  }
  .line-number {
    box-sizing: border-box;
    height: var(--line-height);
    font-size: 16px !important;
    line-height: 16px !important;
    text-align: right !important;
    padding: 2px;
    margin: 0;
    text-align: center;
    border-radius: 6px;
    transition: background 0.2s;
  }
  .line-number:hover {
    background: #fbbf24;
    color: #fff;
  }
  .code-editor {
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace !important;
    line-height: var(--line-height) !important;
    font-size: 14px !important;
    border-radius: 0 12px 12px 0;
    background: inherit;
    transition: box-shadow 0.2s, border 0.2s;
  }
  .code-editor:focus {
    box-shadow: 0 0 0 2px #fb923c;
    border-color: #fb923c;
  }
  .code-editor ::global(.token) {
    line-height: var(--line-height) !important;
    vertical-align: baseline !important;
  }
  .code-editor ::global(.punctuation),
  .code-editor ::global(.operator) {
    vertical-align: text-top;
  }
  .code-editor ::global(br) {
    content: '';
    display: block;
    height: var(--line-height);
  }
  .code-editor {
    white-space: pre-wrap;
    overflow-wrap: break-word;
  }
  .thin-scrollbar {
    scrollbar-width: thin;
    scrollbar-color: #fb923c #23272f;
    overflow-y: auto;
  }
  .thin-scrollbar::-webkit-scrollbar {
    width: 6px;
    height: 6px;
  }
  .thin-scrollbar::-webkit-scrollbar-thumb {
    background: #fb923c;
    border-radius: 4px;
    transition: background 0.3s;
  }
  .thin-scrollbar::-webkit-scrollbar-thumb:hover {
    background: #f59e42;
  }
  .thin-scrollbar::-webkit-scrollbar-track {
    background: #23272f;
  }
  .code-editor:focus {
    box-shadow: none;
    border-color: transparent;
  }
</style>